/*
* This file is part of the GeoLatte project.
*
* GeoLatte is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GeoLatte is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with GeoLatte. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2010 - 2011 and Ownership of code is shared by:
* Qmino bvba - Esperantolaan 4 - 3001 Heverlee (http://www.qmino.com)
* Geovise bvba - Generaal Eisenhowerlei 9 - 2140 Antwerpen (http://www.geovise.com)
*/
package org.geolatte.maprenderer.sld.graphics;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.geolatte.maprenderer.util.SVGDocumentIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGSVGElement;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* A Repository for ExternalGraphic images.
*
* <p>Clients can get {@link ExternalGraphic}s from this repository by URL. The ExternalGraphic is first search in the internal cache,
* then it is search in local symbol packages (see below), finally it is searched on the internet.</p>
*
* <p>An <code>ExternalGraphicsRepository</code> can be provided with a set of local symbol packages. These packages (jars)
* must reside on the class path and contain a properties file 'graphics.index' listing the URL's and image filenames.</p>
*
* @author Karel Maesen, Geovise BVBA
* creation-date: 9/14/11
*/
public class ExternalGraphicsRepository {
public final static float DEFAULT_SIZE = 16f;
public final static float DEFAULT_ROTATION = 0f;
private final static Logger LOGGER = LoggerFactory.getLogger(ExternalGraphicsRepository.class);
private final Map<ImageKey, BufferedImage> cache = new ConcurrentHashMap<ImageKey, BufferedImage>();
private final Map<String, SVGDocument> svgCache = new ConcurrentHashMap<String, SVGDocument>();
//TODO -- verify the exception-handling scenario's
//TODO -- replace the concurrentHashMaps with ehcache (in order to control growth of the cache). Note that ehcache is already a dependency
public ExternalGraphicsRepository(String[] localGraphicsPackage) {
for (String packageName :localGraphicsPackage){
readGraphicsFromPackage(packageName);
}
}
public BufferedImage get(String url) throws IOException {
return get(url, DEFAULT_SIZE, DEFAULT_ROTATION, false);
}
public BufferedImage get(String url, float size, float rotation, boolean sizeSet) throws IOException {
ImageKey key = new ImageKey(url, size, rotation);
BufferedImage image = getFromCache(key);
if (image == null) {
image = retrieve(url, size, sizeSet);
if (rotation != 0) {
image = rotate(image, rotation);
}
storeInCache(key, image);
}
return image;
}
public void storeInCache(ImageKey url, BufferedImage source) {
if (url == null || source == null) throw new IllegalArgumentException();
this.cache.put(url, source);
}
public void storeInSvgCache(String url, SVGDocument svgDoc) {
if (url == null || svgDoc == null) throw new IllegalArgumentException();
this.svgCache.put(url, svgDoc);
}
public BufferedImage getFromCache(ImageKey url) {
return this.cache.get(url);
}
public SVGDocument getSVGFromCache(String url) {
return this.svgCache.get(url);
}
/**
* Reads all the graphics from a package and stores the images in the image cache and svg's
* in the svg cache.
*
* @param packageName
*/
private void readGraphicsFromPackage(String packageName) {
InputStream graphicsIndex = getGraphicsIndex(packageName);
if (graphicsIndex == null) {
LOGGER.warn("Can't find package " + packageName +", or package doesn't have a graphics.index file.");
return;
}
try {
Properties index = readGraphicsIndex(graphicsIndex);
readGraphicsFromPackage(packageName, index);
} catch (IOException e){
LOGGER.warn("Error reading from package " + packageName, e);
}
}
private void readGraphicsFromPackage(String packageName, Properties index) throws IOException {
Enumeration<String> enumeration = (Enumeration<String>)index.propertyNames();
while (enumeration.hasMoreElements()) {
String url = enumeration.nextElement();
String path = packageName + "/" + index.getProperty(url).trim();
retrieveAndStore(url, path);
}
}
private Properties readGraphicsIndex(InputStream graphicsIndexFile) throws IOException {
Properties index = new Properties();
index.loadFromXML(graphicsIndexFile);
return index;
}
/**
* Retrieves image or SVG from path and stores it in the cache (svg or image cache)
* @param uri
* @param path
* @throws IOException
*/
private void retrieveAndStore(String uri, String path) throws IOException {
InputStream inputStream = getResourceAsInputStream(path);
if (inputStream == null) {
throw new IOException(String.format("Graphics file %s not found on classpath.", path));
}
//try to read it as an image (png, jpeg,..)
BufferedImage img = ImageIO.read(inputStream);
if (img != null) {
storeInCache(new ImageKey(uri),img);
return;
}
//ImageIO.read() removes the first 8 characters from the input stream, so we need to reset it.
inputStream = getResourceAsInputStream(path);
//if unsuccesfull, try to read it as an SVG
SVGDocument svg = SVGDocumentIO.read(uri, inputStream);
if (svg != null) {
storeInSvgCache(uri, svg);
return;
}
throw new IOException("File " + path + " is neither image nor svg.");
}
/**
* Retrieves the image from specified URL.
*
* If the url points to an SVG, the SVG is rendered at the specified size. If the
* url points to an bitmap image, then the image is scaled to size only if sizeSet is true.
*
* @param url URL to the graphic source.
* @param size size at which to render the image (for SVG) or to which to scale (if sizeSet)
* @param sizeSet whether or not a specific size is explicitly requested for the image
* @return
* @throws IOException
*/
private BufferedImage retrieve(String url, float size, boolean sizeSet) throws IOException {
//check if we have the image in default_size of
BufferedImage unscaledImage = getFromCache(new ImageKey(url));
if (unscaledImage != null) {
return sizeSet ? scale(unscaledImage, size) : unscaledImage;
}
//check if we have it in the SVG Cache
SVGDocument cachedSVG = this.svgCache.get(url);
if (cachedSVG != null){
return transCodeSVG(cachedSVG, size);
}
//if not, retrieve from URL.
HttpEntity entity = retrieveGraphicFromUrl(url);
if (contentTypeIsSVG(entity)) {
return SVGFromURLResponse(url, size, entity);
} else {
return scale(ImageFromURLResponse(url, entity), size);
}
}
private BufferedImage ImageFromURLResponse(String url, HttpEntity entity) throws IOException {
BufferedImage img = ImageIO.read(entity.getContent());
if (img == null) throw new IOException("Response from " + url + " is not recognized as an image.");
//remember the default image (before rotating and scaling)
storeInCache(new ImageKey(url), img);
return img;
}
private BufferedImage SVGFromURLResponse(String url, float size, HttpEntity entity) throws IOException {
SVGDocument svg = SVGDocumentIO.read(url, entity.getContent());
if (svg == null) throw new IOException("Response from " + url + " is not recognized as SVG document.");
storeInSvgCache(url, svg);
return transCodeSVG(svg, size);
}
private HttpEntity retrieveGraphicFromUrl(String url) throws IOException {
HttpGet httpGet = new HttpGet(url);
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpResponse response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() != 200) {
throw new IOException("Can't retrieve image from " + url);
}
return response.getEntity();
}
private BufferedImage rotate(BufferedImage img, float rotation) {
AffineTransform tx = new AffineTransform();
//determine center point of image
int sx = img.getMinX() + img.getWidth()/2;
int sy = img.getMinY() + img.getHeight()/2;
double theta = Math.toRadians(rotation);
tx.rotate(theta, sx, sy);
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
return op.filter(img, null);
}
private BufferedImage transCodeSVG(SVGDocument svg, float size){
if (size < 0) {
size = DEFAULT_SIZE;
}
SVGTranscoder transcoder = new SVGTranscoder();
SVGSVGElement svgRootElement = svg.getRootElement();
float svgWidth = svgRootElement.getWidth().getBaseVal().getValue();
float svgHeight = svgRootElement.getHeight().getBaseVal().getValue();
// float aspectRatio = svgWidth/svgHeight;
// int height = Math.round(size);
// int width = (int)(aspectRatio * height);
Dimension dim = getWidthAndHeight(svgWidth, svgHeight, size);
return transcoder.transcode(svg, dim.width, dim.height);
}
private BufferedImage scale(BufferedImage unscaledImage, float size) {
Dimension dim = getWidthAndHeight(unscaledImage.getWidth(), unscaledImage.getHeight(), size);
AffineTransform tx = new AffineTransform();
tx.scale(((double)dim.width / unscaledImage.getWidth()), ((double)dim.height) / unscaledImage.getHeight());
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
return op.filter(unscaledImage, null);
}
private Dimension getWidthAndHeight(float originalWidth, float originalHeight, float newSize){
float aspectRatio = originalWidth/originalHeight;
Dimension dim;
int height = Math.round(newSize);
int width = (int)(aspectRatio * height);
return new Dimension(width, height);
}
private boolean contentTypeIsSVG(HttpEntity entity) {
return "image/svg+xml".equalsIgnoreCase(entity.getContentType().getValue());
}
private InputStream getGraphicsIndex(String packageName) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(packageName + "/graphics.index");
}
private InputStream getResourceAsInputStream(String resource) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
}
public static class ImageKey {
private final String url;
private final float size;
private final float rotation;
private ImageKey(String url, float size, float rotation) {
this.url = url;
this.size = size;
this.rotation = rotation;
}
public ImageKey(String url) {
this(url, DEFAULT_SIZE, DEFAULT_ROTATION);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImageKey imageKey = (ImageKey) o;
if (Float.compare(imageKey.rotation, rotation) != 0) return false;
if (Float.compare(imageKey.size, size) != 0) return false;
if (url != null ? !url.equals(imageKey.url) : imageKey.url != null) return false;
return true;
}
@Override
public int hashCode() {
int result = url != null ? url.hashCode() : 0;
result = 31 * result + (size != +0.0f ? Float.floatToIntBits(size) : 0);
result = 31 * result + (rotation != +0.0f ? Float.floatToIntBits(rotation) : 0);
return result;
}
}
}